#pragma once

#include "panicking.h"
#include "ptr.h"
#include "trait.h"

namespace sfc::option {

namespace detail {

template <class T>
struct Inn {
  bool _tag;
  union {
    T _val;
  };

 public:
  [[sfc_inline]] Inn() : _tag{false} {}
  [[sfc_inline]] Inn(T&& val) noexcept : _tag{true}, _val{static_cast<T&&>(val)} {}

  [[sfc_inline]] ~Inn() noexcept = default;
  [[sfc_inline]] ~Inn() requires(!trait::TvDrop<T>) {
    if (_tag) _val.~T();
  }

  [[sfc_inline]] Inn(Inn&& other) noexcept = default;
  [[sfc_inline]] Inn(Inn&& other) noexcept requires(!trait::TvCopy<T>) : _tag{other._tag} {
    if (!_tag) return;
    new (mem::inplace_t{}, &_val) T{static_cast<T&&>(other._val)};
  }

  [[sfc_inline]] Inn(const Inn& other) noexcept = default;
  [[sfc_inline]] Inn(const Inn& other) requires(!trait::Copy<T>) : _tag{other._tag} {
    if (!_tag) return;
    new (mem::inplace_t{}, &_val) T{other._val};
  }

  [[sfc_inline]] Inn& operator=(const Inn&) = default;
  [[sfc_inline]] Inn& operator=(const Inn& other) requires(!trait::TvCopy<T>) {
    if (_tag == other._tag) {
      _tag ? _val = other._val : (void)0;
    } else {
      _tag = other._tag;
      _tag ? new (mem::inplace_t{}, &_val) T{other._val} : _val.~T();
    }
    return *this;
  }

  [[sfc_inline]] Inn& operator=(Inn&&) noexcept = default;
  [[sfc_inline]] Inn& operator=(Inn&& other) noexcept requires(!trait::TvCopy<T>) {
    if (_tag == other._tag) {
      _tag ? _val = static_cast<T&&>(other._val) : (void)0;
    } else {
      _tag = other._tag;
      _tag ? new (mem::inplace_t{}, &_val) T{static_cast<T&&>(other._val)} : _val.~T();
    }
    return *this;
  }

  [[sfc_inline]] explicit operator bool() const {
    return _tag;
  }

  [[sfc_inline]] auto operator*() const -> const T& {
    return _val;
  }

  [[sfc_inline]] auto operator*() -> T& {
    return _val;
  }
};

template <class T>
struct Inn<T&> {
  T* _ptr = nullptr;

 public:
  [[sfc_inline]] Inn() = default;

  [[sfc_inline]] ~Inn() {}

  [[sfc_inline]] Inn(T& val) : _ptr{&val} {}

  [[sfc_inline]] explicit operator bool() const {
    return _ptr != nullptr;
  }

  [[sfc_inline]] auto operator*() const -> const T& {
    return *_ptr;
  }

  [[sfc_inline]] auto operator*() -> T& {
    return *_ptr;
  }
};

}  // namespace detail

template <class T>
class Option {
  detail::Inn<T> _inn;

 public:
  [[sfc_inline]] Option() = default;

  [[sfc_inline]] Option(T val) noexcept : _inn{static_cast<T&&>(val)} {}

  [[sfc_inline]] ~Option() = default;

  [[sfc_inline]] Option(const Option&) noexcept = default;

  [[sfc_inline]] Option(Option&&) noexcept = default;

  [[sfc_inline]] Option& operator=(Option&&) noexcept = default;

  [[sfc_inline]] explicit operator bool() const {
    return bool(_inn);
  }

  [[sfc_inline]] auto is_some() const -> bool {
    return bool(_inn);
  }

  [[sfc_inline]] auto is_none() const -> bool {
    return !_inn;
  }

  [[sfc_inline]] auto get_unchecked() const -> const T& {
    return *_inn;
  }

  [[sfc_inline]] auto get_unchecked_mut() -> T& {
    return *_inn;
  }

  [[sfc_inline]] auto unwrap_unchecked() -> T {
    return static_cast<T&&>(*_inn);
  }

  auto operator*() const -> const T& {
    panicking::assert_fmt(_inn, "Option::operator*: None.");
    return *_inn;
  }

  auto operator*() -> T& {
    panicking::assert_fmt(_inn, "Option::operator*: None.");
    return *_inn;
  }

  auto operator&&(auto optb) const {
    if (!_inn) {
      return decltype(optb){};
    }
    return optb;
  }

  auto operator||(Option optb) const& -> Option {
    if (_inn) {
      *this;
    }
    return optb;
  }

  auto operator||(Option optb) && -> Option {
    if (_inn) {
      return static_cast<Option&&>(*this);
    }
    return optb;
  }

  template <class U>
  auto operator==(const Option<U>& other) const -> bool {
    if (!_inn) {
      return !other;
    }

    return bool(other) && *_inn == *other;
  }

  auto and_then(auto&& f) const& -> decltype(declval<T>()) {
    if (!_inn) {
      return {};
    }
    return f(*_inn);
  }

  auto and_then(auto&& f) && -> decltype(f(declval<T>())) {
    if (!_inn) {
      return {};
    }
    return f(static_cast<T&&>(*_inn));
  }

  auto unwrap() && -> T {
    panicking::assert_fmt(*this, "Option::unwrap: None.");
    return static_cast<T&&>(*_inn);
  }

  auto unwrap() const& -> T {
    panicking::assert_fmt(_inn, "Option::unwrap: None.");
    return *_inn;
  }

  auto unwrap_or(T default_val) && -> T {
    if (!_inn) {
      return default_val;
    }

    return static_cast<T&&>(*_inn);
  }

  auto unwrap_or(T default_val) const& -> T {
    if (!_inn) {
      return default_val;
    }

    return *_inn;
  }

  auto expect(const auto&... msg) && -> T {
    if (!_inn) {
      panicking::panic_fmt(msg...);
    }
    return *_inn;
  }

  auto map(auto&& f) const& -> Option<decltype(f(declval<const T&>()))> {
    if (!_inn) {
      return {};
    }
    return f(*_inn);
  }

  auto map(auto&& f) && -> Option<decltype(f(declval<T>()))> {
    if (!_inn) {
      return {};
    }
    return f(static_cast<T&&>(*_inn));
  }

  void fmt(auto& f) const {
    _inn ? f.write_fmt("Some({})", *_inn) : f.write_str("None()");
  }
};

template <class T>
Option(T) -> Option<T>;

}  // namespace sfc::option
